<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>大模型流式响应演示</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            primary: '#165DFF',
            secondary: '#0FC6C2',
            neutral: '#F5F7FA',
            dark: '#1D2129',
          },
          fontFamily: {
            inter: ['Inter', 'system-ui', 'sans-serif'],
          },
        },
      }
    };
  </script>
  <style type="text/tailwindcss">
    @layer utilities {
      .content-auto {
        content-visibility: auto;
      }

      .typing-indicator {
        display: inline-flex;
        align-items: center;
      }

      .typing-indicator span {
        height: 6px;
        width: 6px;
        margin: 0 1px;
        background-color: #165DFF;
        border-radius: 50%;
        display: inline-block;
        animation: typing-bounce 1.4s infinite ease-in-out both;
      }

      .typing-indicator span:nth-child(1) {
        animation-delay: -0.32s;
      }

      .typing-indicator span:nth-child(2) {
        animation-delay: -0.16s;
      }

      @keyframes typing-bounce {
        0%, 80%, 100% {
          transform: scale(0);
        }
        40% {
          transform: scale(1);
        }
      }
      .message-appear {
        animation: message-fade-in 0.3s ease-out forwards;
      }

      @keyframes message-fade-in {
        from {
          opacity: 0;
          transform: translateY(10px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }
    }
  </style>
</head>
<body class="bg-neutral min-h-screen font-inter text-dark">
<div class="max-w-4xl mx-auto p-4 md:p-6 flex flex-col min-h-screen">
  <!-- 头部 -->
  <header class="mb-6 text-center">
    <h1 class="text-[clamp(1.5rem,3vw,2.5rem)] font-bold text-primary mb-2">大模型流式响应演示</h1>
    <p class="text-gray-600">体验AI实时生成内容的流畅感</p>
  </header>

  <!-- 聊天区域 -->
  <main class="flex-grow bg-white rounded-xl shadow-md p-4 md:p-6 mb-6 overflow-hidden">
    <div id="chat-container" class="space-y-4 max-h-[60vh] overflow-y-auto pr-2">
      <div class="flex items-start mb-4 message-appear">
        <div class="bg-primary/10 p-3 rounded-lg max-w-[90%] md:max-w-[80%]">
          <p class="text-gray-700">你好！我是AI助手。向我提问，我会以流式方式回复你。</p>
        </div>
      </div>
    </div>
  </main>

  <!-- 输入区域 -->
  <footer class="relative">
    <div class="flex items-end space-x-3">
        <textarea
          id="user-input"
          class="flex-grow p-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none min-h-[60px]"
          placeholder="输入问题，按Shift+Enter换行，按Enter发送"
        ></textarea>
      <button
        id="send-button"
        class="bg-primary text-white p-3 rounded-lg hover:bg-primary/90 transition-all duration-300 flex items-center justify-center min-w-[60px]"
      >
        <i class="fa fa-paper-plane"></i>
      </button>
    </div>
    <p class="text-xs text-gray-500 mt-2 text-right">提示：支持流式响应的API才能体验实时生成效果</p>
  </footer>
</div>

<script>
  document.addEventListener('DOMContentLoaded', () => {
    const chatContainer = document.getElementById('chat-container');
    const userInput = document.getElementById('user-input');
    const sendButton = document.getElementById('send-button');

    // 发送消息
    const sendMessage = async () => {
      const message = userInput.value.trim();
      if (!message) return;

      // 添加用户消息到聊天界面
      addMessageToChat('user', message);
      userInput.value = '';

      // 添加AI正在输入的指示器
      const aiMessageElement = addTypingIndicator();

      try {
        // 调用API（这里使用模拟API，实际使用时替换为真实API）
        const response = await streamMessageFromAI(message, aiMessageElement);
        if (!response.ok) {
          throw new Error(`API请求失败: ${response.status}`);
        }
      } catch (error) {
        console.error('Error:', error);
        updateTypingIndicator(aiMessageElement, '抱歉，请求处理出错。');
      }
    };

    // 添加消息到聊天界面
    const addMessageToChat = (sender, content) => {
      const messageDiv = document.createElement('div');
      messageDiv.className = 'flex items-start mb-4 message-appear';

      let bgClass, iconClass;
      if (sender === 'user') {
        bgClass = 'bg-primary text-white';
        iconClass = 'fa-user';
        messageDiv.classList.add('justify-end');
      } else {
        bgClass = 'bg-gray-100 text-gray-800';
        iconClass = 'fa-robot';
      }

      messageDiv.innerHTML = `
          <div class="flex items-start space-x-2">
            <div class="w-8 h-8 rounded-full ${bgClass} flex items-center justify-center flex-shrink-0">
              <i class="fa ${iconClass}"></i>
            </div>
            <div class="${sender === 'user' ? 'text-right' : ''}">
              <div class="${bgClass.replace('bg-', 'bg-')} p-3 rounded-lg max-w-[90%] md:max-w-[80%] break-words">
                ${content}
              </div>
            </div>
          </div>
        `;

      chatContainer.appendChild(messageDiv);
      chatContainer.scrollTop = chatContainer.scrollHeight;
      return messageDiv;
    };

    // 添加AI正在输入的指示器
    const addTypingIndicator = () => {
      const typingHTML = `
          <div class="flex items-start space-x-2">
            <div class="w-8 h-8 rounded-full bg-gray-100 text-gray-800 flex items-center justify-center flex-shrink-0">
              <i class="fa fa-robot"></i>
            </div>
            <div>
              <div class="bg-gray-100 p-3 rounded-lg max-w-[90%] md:max-w-[80%]">
                <div class="typing-indicator">
                  <span></span>
                  <span></span>
                  <span></span>
                </div>
              </div>
            </div>
          </div>
        `;

      chatContainer.insertAdjacentHTML('beforeend', typingHTML);
      chatContainer.scrollTop = chatContainer.scrollHeight;

      // 返回包含指示器的元素，以便后续更新
      return chatContainer.lastElementChild;
    };

    // 更新AI正在输入的指示器为实际内容
    const updateTypingIndicator = (indicatorElement, val) => {
      const messageContentDiv = indicatorElement.querySelector('.bg-gray-100.p-3');
      const html = typeof val === "string" ? val : (() => {
        const { thinkText, assistantText } = val;
        const ret = [];
        if (!!thinkText.trim().length) {ret.push(`<div style="font-size: 12px; color: #333">${thinkText}</div>`);}
        if (!!assistantText.trim().length) {ret.push(`<div style="font-size: 16px; color: #000000">${assistantText}</div>`);}
        return ret.join('\n');
      })();
      messageContentDiv.innerHTML = html;
      indicatorElement.classList.add('message-appear');
    };

    // 从AI流式获取消息
    const streamMessageFromAI = async (message, indicatorElement) => {
      // 实际使用时替换为真实的API端点
      // 这里使用mockapi.io的模拟API，仅作演示，不会返回真实的AI响应
      // 真实场景应替换为类似OpenAI的API端点
      const apiUrl = 'http://localhost:1234/v1/chat/completions'; // 模拟5秒延迟

      // 构建请求体
      const requestBody = {
        messages: [
          { role: 'system', content: '你回答的语言与用户的语言要保持一致' },
          { role: 'user', content: message }
        ],
        stream: true // 请求流式响应
      };

      // 发送请求
      const response = await fetch(apiUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          // 'Authorization': 'Bearer YOUR_API_KEY' // 取消注释并添加你的API密钥
        },
        body: JSON.stringify(requestBody)
      });

      if (!response.ok || !response.body) {
        updateTypingIndicator(indicatorElement, '抱歉，未能获取到响应。');
        return response;
      }

      // 创建一个解码器来处理UTF-8文本
      const decoder = new TextDecoder();

      let isThinking = false;
      let thinkText = '';
      let assistantText = '';

      // 读取响应流
      const reader = response.body.getReader();
      let done = false;

      while (!done) {
        const { value, done: readerDone } = await reader.read();
        done = readerDone;

        if (value) {
          // 解码接收到的字节
          let chunk = decoder.decode(value, { stream: true });
          const strList = chunk.split(/\n/).filter(i => !!i.trim().length);

          const text = strList.map(str => {
            if (str === 'data: [DONE]') {return '';}
            return JSON.parse(str.slice('data: '.length));
          }).map(i => i.choices?.[0]?.delta?.content).filter(Boolean).join('');

          // console.log(isThinking, text);
          if (isThinking) {
            thinkText += text;
          } else {
            assistantText += text;
          }

          if (text === '<think>') {
            isThinking = true;
          }
          if (text.slice(-8) === '</think>') {
            isThinking = false;
          }
          console.log(text);
          // 处理SSE格式的数据（data: {...}\n\n）
          // 这里只是简单地将所有内容连接起来，实际处理可能需要根据API返回格式调整
          // assistantText += text;

          // 更新聊天界面
          updateTypingIndicator(indicatorElement, { thinkText, assistantText });

          // 滚动到底部
          chatContainer.scrollTop = chatContainer.scrollHeight;
        }
      }

      return response;
    };

    // 事件监听
    sendButton.addEventListener('click', sendMessage);

    userInput.addEventListener('keydown', (e) => {
      // 按Enter发送消息，Shift+Enter换行
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        sendMessage();
      }
    });

    // 自动聚焦到输入框
    userInput.focus();
  });
</script>
</body>
</html>
